import {Subject} from "../Subject";
import {ObjectLiteral} from "../../common/ObjectLiteral";

/**
 * Finds all cascade operations of the given subject and cascade operations of the found cascaded subjects,
 * e.g. builds a cascade tree and creates a subjects for them.
 */
export class CascadesSubjectBuilder {

    // ---------------------------------------------------------------------
    // Constructor
    // ---------------------------------------------------------------------

    constructor(protected allSubjects: Subject[]) {
    }

    // ---------------------------------------------------------------------
    // Public Methods
    // ---------------------------------------------------------------------

    /**
     * Builds a cascade subjects tree and pushes them in into the given array of subjects.
     */
    build(subject: Subject, operationType: "save"|"remove"|"soft-remove"|"recover") {

        subject.metadata
            .extractRelationValuesFromEntity(subject.entity!, subject.metadata.relations) // todo: we can create EntityMetadata.cascadeRelations
            .forEach(([relation, relationEntity, relationEntityMetadata]) => {

                // we need only defined values and insert, update, soft-remove or recover cascades of the relation should be set
                if (relationEntity === undefined ||
                    relationEntity === null ||
                    (!relation.isCascadeInsert && !relation.isCascadeUpdate && !relation.isCascadeSoftRemove && !relation.isCascadeRecover))
                    return;

                // if relation entity is just a relation id set (for example post.tag = 1)
                // then we don't really need to check cascades since there is no object to insert or update
                if (!(relationEntity instanceof Object))
                    return;

                // if we already has this entity in list of operated subjects then skip it to avoid recursion
                const alreadyExistRelationEntitySubject = this.findByPersistEntityLike(relationEntityMetadata.target, relationEntity);
                if (alreadyExistRelationEntitySubject) {
                    if (alreadyExistRelationEntitySubject.canBeInserted === false) // if its not marked for insertion yet
                        alreadyExistRelationEntitySubject.canBeInserted = relation.isCascadeInsert === true && operationType === "save";
                    if (alreadyExistRelationEntitySubject.canBeUpdated === false) // if its not marked for update yet
                        alreadyExistRelationEntitySubject.canBeUpdated = relation.isCascadeUpdate === true && operationType === "save";
                    if (alreadyExistRelationEntitySubject.canBeSoftRemoved === false) // if its not marked for removal yet
                        alreadyExistRelationEntitySubject.canBeSoftRemoved = relation.isCascadeSoftRemove === true && operationType === "soft-remove";
                    if (alreadyExistRelationEntitySubject.canBeRecovered === false) // if its not marked for recovery yet
                        alreadyExistRelationEntitySubject.canBeRecovered = relation.isCascadeRecover === true && operationType === "recover";
                    return;
                }

                // mark subject with what we can do with it
                // and add to the array of subjects to load only if there is no same entity there already
                const relationEntitySubject = new Subject({
                    metadata: relationEntityMetadata,
                    parentSubject: subject,
                    entity: relationEntity,
                    canBeInserted: relation.isCascadeInsert === true && operationType === "save",
                    canBeUpdated: relation.isCascadeUpdate === true && operationType === "save",
                    canBeSoftRemoved: relation.isCascadeSoftRemove === true && operationType === "soft-remove",
                    canBeRecovered: relation.isCascadeRecover === true && operationType === "recover"
                });
                this.allSubjects.push(relationEntitySubject);

                // go recursively and find other entities we need to insert/update
                this.build(relationEntitySubject, operationType);
            });
    }

    // ---------------------------------------------------------------------
    // Protected Methods
    // ---------------------------------------------------------------------

    /**
     * Finds subject where entity like given subject's entity.
     * Comparision made by entity id.
     */
    protected findByPersistEntityLike(entityTarget: Function|string, entity: ObjectLiteral): Subject|undefined {
        return this.allSubjects.find(subject => {
            if (!subject.entity)
                return false;

            if (subject.entity === entity)
                return true;

            return subject.metadata.target === entityTarget && subject.metadata.compareEntities(subject.entityWithFulfilledIds!, entity);
        });
    }

}